package org.arclib.x.sprite;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.log4j.Logger;
import org.arclib.Texture;
import org.arclib.draw.Draw;
import org.arclib.math.Collision;
import org.arclib.math.Common;
import org.arclib.math.Point;
import org.arclib.math.Size;
import org.arclib.sound.Sound;
import org.arclib.time.Blinker;
import org.arclib.x.sprite.frame.COLLISION_TYPE;
import org.arclib.x.sprite.frame.Frame;

/// A graphical object with animation and sound
public class Sprite extends Entity
{
	// / box/radius constructor, load from filename
	public Sprite(String filename, COLLISION_TYPE argCol) throws IOException
	{
		// set collision type
		col = argCol;
		addFrame(filename, DEFAULT_ANIM, DEFAULT_TIME, null);
		initialize();
	}

	// / box/radius constructor, load from texture
	public Sprite(Texture tex, COLLISION_TYPE argCol)
	{
		// set collision type
		col = argCol;
		addFrame(tex, DEFAULT_ANIM, DEFAULT_TIME, null);
		initialize();
	}

	// / simple box/rad constructor, do not load anything
	public Sprite(COLLISION_TYPE argCol)
	{
		col = argCol;
		initialize();
	}

	// / what everything needs to do to initialize itself
	void initialize()
	{
		anim = new HashMap<String, Animation>();
		zoomX = zoomY = 1;
		rotSpeed = new Blinker();
		rotPoint = new ArrayList<RotationPoint>();
	}

	// / add a frame to the animation
	public void addFrame(String img, String animation, int time, Sound snd)
			throws IOException
	{
		// allocate new animation as required
		if (!(anim.containsKey(animation)))
		{
			anim.put(animation, new Animation());
		}
		// otherwise just add it and set curr animation to it
		currAnim = animation;

		// load frame and set size
		// Size s = anim.get(animation).addFrame(img, time, snd, col);
		Animation a = (Animation) anim.get(animation);
		Size s = a.addFrame(img, time, snd, col);

		// set getWidth from one extracted from animation
		setSize(s.w, s.h);
		origW = s.w;
		origH = s.h;

		// calculate radius if necessary
		if (col == COLLISION_TYPE.CIRCLE)
			calcRadius();
	}

	// / add a frame to the animation
	public void addFrame(Texture tex, String animation, int time, Sound snd)
	{
		// allocate new animation as required
		if (!(anim.containsKey(animation)))
		{
			anim.put(animation, new Animation());
		}
		// otherwise just add it and set curr animation to it
		currAnim = animation;

		// load frame and set size
		// Size s = anim.get(animation).addFrame(tex, time, snd, col);
		Animation a = (Animation) anim.get(animation);
		Size s = a.addFrame(tex, time, snd, col);

		// set getWidth from one extracted from animation
		setSize(s.w, s.h);
		origW = s.w;
		origH = s.h;

		// calculate radius if necessary
		if (col == COLLISION_TYPE.CIRCLE)
			calcRadius();
	}

	// / returns true if point getX, getY collides with sprite
	public boolean collide(double argX, double argY)
	{
		if (col == COLLISION_TYPE.BOX)
		{
			return Collision.boxXYCollision(new Point(argX, argY), new Point(
					getLeftX(), getTopY()), new Size(getWidth(), getHeight()));
		}

		if (col == COLLISION_TYPE.CIRCLE)
		{
			return Collision.circleXYCollision(new Point(argX, argY),
					new Point(getLeftX() + getHalfWidth(), getTopY()
							+ getHalfHeight()), radius);
		}

		log.info("no collision type found");
		return false;
	}

	// / collide a sprite with any other sprite
	public boolean collide(Sprite s)
	{
		if (col == COLLISION_TYPE.BOX)
		{
			// box-box
			if (s.col == COLLISION_TYPE.BOX)
			{
				return Collision.boxBoxCollision(new Point(getLeftX(),
						getTopY()), new Size(getWidth(), getHeight()),
						new Point(s.getLeftX(), s.getTopY()), new Size(s
								.getWidth(), s.getHeight()));
			}
			// box-rad
			else if (s.col == COLLISION_TYPE.CIRCLE)
			{
				return Collision.boxCircleCollision(new Point(getLeftX(),
						getTopY()), new Size(getWidth(), getHeight()),
						new Point(s.getLeftX() + s.getHalfWidth(), s.getTopY()
								+ s.getHalfHeight()), s.getRadius());
			}
		} else if (col == COLLISION_TYPE.CIRCLE)
		{
			// test circle
			if (s.col == COLLISION_TYPE.CIRCLE)
			{
				return Collision.circleCircleCollision(new Point(getLeftX()
						+ getHalfWidth(), getTopY() + getHalfHeight()), radius,
						new Point(s.getLeftX() + s.getHalfWidth(), s.getTopY()
								+ s.getHalfHeight()), s.getRadius());
			}
			if (s.col == COLLISION_TYPE.BOX)
			{
				return Collision.boxCircleCollision(new Point(s.getLeftX(), s
						.getTopY()), new Size(s.getWidth(), s.getHeight()),
						new Point(getLeftX() + getHalfWidth(), getTopY()
								+ getHalfHeight()), radius);
			}
		}

		log.info("collision failed");

		return false;
	}

	// / draw frame
	public void draw()
	{
		((Animation) anim.get(currAnim)).draw(getX(), getY(), getWidth(),
				getHeight(), getColor(), pivX, pivY, getAngleRad(), getLeftX(),
				getTopY(), col);
	}

	// / draw the boundry of given type
	public void drawBounds()
	{
		if (col == COLLISION_TYPE.BOX)
			Draw.rectangle(new Point(getLeftX(), getTopY()), new Size(
					getWidth(), getHeight()), getColor(), false);
		else if (col == COLLISION_TYPE.CIRCLE)
		{
			Draw.circle(new Point(getLeftX() + getHalfWidth(), getTopY()
					+ getHalfHeight()), radius, 30, getColor(), false);
		}
	}

	// / fast angle setting in double
	public void setFastAngleRad(double argA)
	{
		angle = argA;
	}

	// TODO: is this a sensible method name? (see setFastAngle above)
	// / rotate frame to specified degree in double
	public void setAngleRad(double argA)
	{
		// rotate exactly to it
		if (rotateBy == 0)
		{
			angle = argA;
			Common.restrictRadian(angle);
		}
		// slowly rotate by setting target getAngle
		else
		{
			targetAngle = argA;
			Common.restrictRadian(targetAngle);
		}
	}

	// / get getAngle at which it is rotated at in double
	public double getAngleRad()
	{
		return angle;
	}

	// / fast angle setting in degrees
	public void setFastAngleDeg(double argA)
	{
		angle = Common.degreesToRadians(argA);
	}

	// TODO: is this a sensible method name? (see setFastAngle above)
	// / rotate frame to specified degree in degrees
	public void setAngleDeg(double argA)
	{
		argA = Common.degreesToRadians(argA);

		// rotate exactly to it
		if (rotateBy == 0)
		{
			angle = argA;
			Common.restricDegree(angle);
		}
		// slowly rotate by setting target getAngle
		else
		{
			targetAngle = argA;
			Common.restricDegree(targetAngle);
		}
	}

	// / get getAngle at which it is rotated at in degrees
	public double getAngleDeg()
	{
		return Common.radiansToDegrees(angle);
	}

	// / point on which to rotate around
	public void setPivot(double argX, double argY)
	{
		pivX = argX;
		pivY = argY;
	}

	// / set pivot point with point
	public void setPivot(Point p)
	{
		pivX = p.x;
		pivY = p.y;
	}

	// / set X pivot point
	public void setPivX(double argX)
	{
		pivX = argX;
	}

	// / set Y pivot point
	public void setPivY(double argY)
	{
		pivY = argY;
	}

	// / returns X rotation pivot point
	public double getPivX()
	{
		return pivX;
	}

	// / returns Y rotation pivot point
	public double getPivY()
	{
		return pivY;
	}

	// / get radius
	public double getRadius()
	{
		return radius;
	}

	// / set radius
	public void setRadius(double argR)
	{
		radius = argR;
	}

	// / return the current frame in the sprite
	public Frame getCurrFrame()
	{
		return ((Animation) anim.get(currAnim)).getCurrFrame();
	}

	// / set animation loop/not loop
	public void setAnimLoop(String animation, boolean v)
	{
		((Animation) anim.get(animation)).setLoop(v);
	}

	// / get zoom X value
	public double getZoomX()
	{
		return zoomX;
	}

	// / get zoom Y value
	public double getZoomY()
	{
		return zoomY;
	}

	// / set getWidth of sprite
	public void setWidth(double argW)
	{
		setSize(argW, getHeight());
	}

	// / set getHeight of sprite
	public void setHeight(double argH)
	{
		setSize(getWidth(), argH);
	}

	// / set size of sprite
	public void setSize(double argW, double argH)
	{
		super.setSize(argW, argH);

		// calculate zoom amount
		zoomX = argW / origW;
		zoomY = argH / origH;
		if (col == COLLISION_TYPE.CIRCLE)
		{
			calcRadius();
		}
	}

	// / set general zoom
	public void setZoom(double argZ)
	{
		setZoom(argZ, argZ);
	}

	// / set zoom X and Y amounts
	public void setZoom(double argZX, double argZY)
	{
		zoomX = argZX;
		zoomY = argZY;

		super.setSize(zoomX * origW, zoomY * origH);

		if (col == COLLISION_TYPE.CIRCLE)
		{
			calcRadius();
		}
	}

	// / give a certain sound to a frame
	public void setSound(Sound snd, String animation, int argNum)
	{
		((Animation) anim.get(animation)).setSound(snd, argNum);
	}

	// / retrieve a certain sound from a frame
	public Sound getSound(String animation, int argNum)
	{
		return ((Animation) anim.get(animation)).getSound(argNum);
	}

	// / set frame to display for time amount
	public void setTime(int argT, String animation, int argNum)
	{
		((Animation) anim.get(animation)).setTime(argT, argNum);
	}

	// / gives the current time display of frame
	public int getTime(String animation, int argNum)
	{
		return ((Animation) anim.get(animation)).getTime(argNum);
	}

	// / remove an animation
	public void removeAnim(String animation)
	{
		anim.remove(animation);
	}

	// / set current animation
	public void setAnim(String animation)
	{
		currAnim = animation;
	}

	// / reset an animation
	public void resetAnim(String animation)
	{
		((Animation) anim.get(animation)).reset();
	}

	// / get current animation
	public String getAnim()
	{
		return currAnim;
	}

	// / get current animation frame number
	public int getAnimNum()
	{
		return ((Animation) anim.get(currAnim)).getCurr();
	}

	// / move based on getAngle facing, -1 is backwards, 1 is forwards
	public void moveDir(double argSpeed)
	{
		setX(getX() + Math.sin(-getAngleRad()) * -argSpeed);
		setY(getY() + Math.cos(-getAngleRad()) * -argSpeed);
	}

	// / move based on a velocity vector
	public void moveVel(Point vel)
	{
		setX(getX() + vel.x);
		setY(getY() + vel.y);
	}

	// / get velocity based on angle and speed
	public Point getVelocityDir(double argSpeed)
	{
		return new Point(Math.sin(-targetAngle) * -argSpeed, Math
				.cos(-targetAngle)
				* -argSpeed);
	}

	// / set look gradient, the number of degrees to rotate as we face a point
	public void setRotateBy(double argLG)
	{
		rotateBy = argLG;
	}

	// / return the value of the processPosition amount
	public double getRotateBy()
	{
		return rotateBy;
	}

	// / set value of the rotation speed
	public void setRotateSpeed(double argSpeed)
	{
		speedOfRot = argSpeed;
	}

	// / get value of rotation speed
	public double getRotateSpeed()
	{
		return speedOfRot;
	}

	// / have sprite face towards the direction of X,Y
	public void pointTo(double argX, double argY)
	{
		// / thank you, phr00t!
		targetAngle = /* degreeToRadian( */Math.PI
				- Math.atan2(argX - getX(), argY - getY());// );

		// if rotateBy is just zero, then rotate directly there
		// otherwise it will be slowly rotated in 'process()'
		if (rotateBy == 0)
			angle = targetAngle;
	}

	// / have sprite face towards another sprite
	public void pointTo(Sprite s)
	{
		pointTo(s.getX(), s.getY());
	}

	// / point to specific point
	public void pointTo(Point argP)
	{
		pointTo(argP.x, argP.y);
	}

	// / just processes pivot points used for collision detection
	public void processCollisionPos()
	{
		calcPivot();
	}

	// / just processes animations
	public void processAnimation()
	{
		((Animation) anim.get(currAnim)).process();
	}

	// / just process rotation and turning
	public void processRotation()
	{
		rotSpeed.process(speedOfRot);

		// inRange function doesn't work here for some unknown reason
		if (Common.withinRange(angle, targetAngle, rotateBy))
		{
			angle = targetAngle;
		}
		// not in range
		else if (rotateBy != 0 && rotSpeed.on
				&& !(angle == 0 && targetAngle == 2 * Math.PI)
				&& !(targetAngle == 0 && angle == 2 * Math.PI))
		{
			double a = 2 * Math.PI - targetAngle + angle;
			double b = targetAngle - angle;

			if (a > 2 * Math.PI)
				a -= 2 * Math.PI;

			if (b < 0)
				b += 2 * Math.PI;

			if (a >= b)
			{
				angle += rotateBy;
				Common.restrictRadian(angle);
			} else if (a < b)
			{
				angle -= rotateBy;
				Common.restrictRadian(angle);
			}
		}
	}

	// / rotate sprite left
	public void rotateLeft()
	{
		setAngleRad(getAngleRad() - rotateBy);
	}

	// / rotate sprite right
	public void rotateRight()
	{
		setAngleRad(getAngleRad() + rotateBy);
	}

	// / process pivots, calculates pivot point and animations
	public void process()
	{
		// calculate pivot point
		processCollisionPos();

		// animations
		processAnimation();

		// rotations
		processRotation();

		// rotation points
		processRotationPoint();
	}

	// / add rotation point
	public void addRotationPoint(double argX, double argY)
	{
		RotationPoint p = new RotationPoint();
		p.init(argX, argY);

		rotPoint.add(p);
	}

	// / get rotation point
	public Point getRotationPoint(int argIndex)
	{
		assert (argIndex < rotPoint.size() && argIndex >= 0);
		// return point
		return new Point(rotPoint.get(argIndex).newPoint.x, rotPoint
				.get(argIndex).newPoint.y);
	}

	// / process rotation points
	public void processRotationPoint()
	{
		// calculate all rotation points
		for (int i = 0; i < rotPoint.size(); i++)
		{
			Point point = rotPoint.get(i).orig.rotateCopy(
					new Point(pivX, pivY), getAngleRad());
			rotPoint.get(i).newPoint = point.add(getPosition());
		}
	}

	// / get leftmost getX value of sprite
	public double getLeftX()
	{
		return leftX;
	}

	// / get topmost getY value of sprite
	public double getTopY()
	{
		return topY;
	}

	// calculate pivot point
	private void calcPivot()
	{
		if (pivX != 0 && pivY != 0)
		{
			// calculate rotations based on pivot points
			Point p1;
			Point p2;

			p1 = Point.fromPolar(pivX, getAngleRad());
			p2 = Point.fromPolar(pivY, getAngleRad());

			// points with calculated pivot point
			leftX = getX() - getHalfWidth() - p2.y + p1.x;
			topY = getY() - getHalfHeight() + p2.x + p1.y;
		} else
		{
			// these will put it in the right position
			leftX = getX() - getHalfWidth();
			topY = getY() - getHalfHeight();
		}
	}

	// calculate radius of sprite
	private void calcRadius()
	{
		radius = (getWidth() + getHeight()) / 4;
	}

	// animation information
	COLLISION_TYPE col;
	// Animation[String] anim;
	HashMap<String, Animation> anim;

	String currAnim = "default";
	double angle = 0;

	// zoom var
	double zoomX = 1, zoomY = 1;

	// special rotational values
	double pivX = 0, pivY = 0;
	double leftX = 0, topY = 0;

	// special vars for radius
	double radius = 0;

	// special vars for pixframe
	double origW, origH;

	// number of double to rotate each time as we point to an object
	double rotateBy = 0;
	// target getAngle we are trying to reach
	double targetAngle = 0;
	// speed of rotation, time based
	double speedOfRot = .1;
	// rotation timer
	Blinker rotSpeed;

	// rotation points that sprite can keep track of
	ArrayList<RotationPoint> rotPoint;

	public static Logger log = Logger.getLogger(Sprite.class);
	final int DEFAULT_TIME = -1;
	final String DEFAULT_ANIM = "default";
}